/* 
 * 	This file is part of VPN Monitor Project 7..

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package monitor.traffic;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

import monitor.Monitor;
import monitor.interfaces.TrafficBufferPackage;
import monitor.interfaces.TrafficStringPackage;
import monitor.interfaces.TrafficPackage;
import monitor.interfaces.EnumTypes.Types;
import monitor.interfaces.EnumTypes.Keys;
import monitor.interfaces.observers.TrafficObserver;
import monitor.interfaces.observers.TrafficStringObserver;
import monitor.interfaces.observers.TrafficBufferObserver;
import monitor.utility.InterfaceNotFoundException;
import monitor.utility.SerializationIO;
import monitor.utility.Time;
import monitor.utility.Utility;

public class Worker{
	private final static long TIME_DELAY = 1000*10;
	private Traffic[] traffic = null;
	private TrafficBuffer[] buffer = null;
	private TrafficStrings[] strings = null;
	private Timer timer = null;
	private TimerTask timeTT = null, trafficTT = null;
	private String trafficNetwork = null;
	private Monitor monitor = null;
	
	public Worker(Monitor monitor, String trafficNetwork, int sampleSize, long delay, HashMap<String, Boolean> limitActive, HashMap<String, Long> limitValue, HashMap<String, Long> logValue){
		this.monitor = monitor;
		this.trafficNetwork = trafficNetwork;
		
		traffic = loadTraffic(trafficNetwork);
		buffer = createTrafficBuffer(sampleSize);
		strings = createTrafficStrings();

		changeLimits(limitValue);
		changeActiveLimits(limitActive);
		changeLogLevel(logValue);		
		
		timeTT = new TimeTT(traffic[0].getDates());
		trafficTT = new TrafficTT(trafficNetwork);
		startStop(true, delay);
	}
	
	/**
	 * Forwarding the request to TrafficTT.
	 * @param ob Observer to be added.
	 */
	public synchronized void addTrafficBufferObserver(TrafficBufferObserver ob){
		((TrafficTT) trafficTT).addTrafficBufferObserver(ob);
	}
	
	/**
	 * Forwarding the request to TrafficTT.
	 * @param ob Observer to be added.
	 */
	public synchronized void addTrafficObserver(TrafficObserver ob){
		((TrafficTT) trafficTT).addTrafficObserver(ob);
	}
	
	/**
	 * Forwarding the request to TrafficTT.
	 * @param ob Observer to be added.
	 */
	public synchronized void addTrafficStringObserver(TrafficStringObserver ob){
		((TrafficTT) trafficTT).addTrafficStringObserver(ob);
	}
	
	/**
	 * Halt Worker and store traffic data for the interface to file.
	 */
	public void halt(){
		startStop(false, -1);
		storeTraffic(trafficNetwork, traffic);
	}
	
	public void changeLogLevel(HashMap<String, Long> logLevel){
		EnumMap<Types, Long> logs = new EnumMap<Types, Long>(Types.class);
		for(Map.Entry<String, Long> e : logLevel.entrySet()){
			Types type = Types.valueOf(e.getKey().toUpperCase());
			logs.put(type, e.getValue());
		}
		for(TrafficStrings ts : strings)
			ts.changeLogLevel(logs.get(ts.getType()));
	}
	
	public void changeActiveLimits(HashMap<String, Boolean> limitActive){
		EnumMap<Types, EnumMap<Keys, Boolean>> activeTypes = new EnumMap<Types, EnumMap<Keys, Boolean>>(Types.class);
		for(Types type : Types.values()){
			EnumMap<Keys, Boolean> activeKey = new EnumMap<Keys, Boolean>(Keys.class);
			for(Keys key : Keys.limits()){
				String k = key.toString().toUpperCase()+"_"+type.toString().toUpperCase();
				if(limitActive.containsKey(k))
					activeKey.put(key, limitActive.get(k));
			}
			activeTypes.put(type, activeKey);
		}
		for(Traffic t : this.traffic)
			t.changeActiveLimits(activeTypes.get(t.getType()));
	}
	
	public void changeLimits(HashMap<String, Long> limitValue){
		EnumMap<Types, EnumMap<Keys, Long>> limitTypes = new EnumMap<Types, EnumMap<Keys, Long>>(Types.class);
		for(Types type : Types.values()){
			EnumMap<Keys, Long> limitKey = new EnumMap<Keys, Long>(Keys.class);
			for(Keys key : Keys.limits()){
				String k = key.toString().toUpperCase()+"_"+type.toString().toUpperCase();
				if(limitValue.containsKey(k))
					limitKey.put(key, limitValue.get(k));
			}
			limitTypes.put(type, limitKey);
		}
		for(Traffic t : this.traffic)
			t.changeLimits(limitTypes.get(t.getType()));
	}
	
	/**
	 * Changes how frequently traffic data are updated,
	 * this is done by canceling all tasks and then reschedule them.
	 * @param delay How frequently traffic data should be updated, in milliseconds.
	 */
	public void changeDelay(long delay){
		startStop(false, delay);
		startStop(true, delay);
	}
	
	/**
	 * Change how many sample TrafficBuffer should take before adding a new value.
	 * @param sample Sample size, value should be greater then 0.
	 */
	public void changeSample(int sample){
		TrafficBuffer.changeSample(sample);
	}
		
	public void changeNetwork(String network, int sampleSize, long delay){
		if(!trafficNetwork.equals(network)){
			monitor.addLogEntry("New network, saving previous data to file "+trafficNetwork+".data");
			storeTraffic(trafficNetwork, traffic);
			trafficNetwork = network;
			traffic = loadTraffic(network);
			buffer = createTrafficBuffer(sampleSize);
		}
		startStop(true, delay);
	}
	
	private TrafficBuffer[] createTrafficBuffer(int sampleSize){
		Types[] type = Types.values();
		TrafficBuffer[] buffer = new TrafficBuffer[type.length];
		for(int i=0;i<type.length;i++)
			buffer[i] = new TrafficBuffer(type[i], sampleSize);
		return buffer;
	}
	
	private TrafficStrings[] createTrafficStrings(){
		Types[] type = Types.values();
		TrafficStrings[] strings = new TrafficStrings[type.length];
		for(int i=0;i<type.length;i++)
			strings[i] = new TrafficStrings(type[i]);
		return strings;
	}
	
	/**
	 * Load old traffic data for the specific interface from file if it exist
	 * otherwise will new instances of Traffic be created for every value in Types.
	 * @param network Interface that traffic data should be restored from.
	 * @return The loaded Traffic if it exist otherwise new instances for every value in Types.
	 */
	private Traffic[] loadTraffic(String network){
		try {
			Traffic[] data = (new SerializationIO<Traffic[]>()).readClassObject(network+".data");
			monitor.addLogEntry("Traffic data for "+network+" was loaded from "+network+".data");
			return data;
		} catch (FileNotFoundException e) {
			//e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		monitor.addLogEntry("No old data was found for "+network+", creating new...");
		Types[] types = Types.values();
		Traffic[] traffic= new Traffic[types.length];
		for(int i=0;i<types.length;i++)
			traffic[i] = new Traffic(types[i]);
		return traffic;
	}
	
	
	/**
	 * Saves current traffic data for active interface to file, TrafficStrings
	 * and TrafficBuffer are not saved.
	 * @param network For what interface data is stored, the file is labeled with this name,
	 * @param traffic The traffic that shall be saved.
	 */
	private void storeTraffic(String network, Traffic[] traffic){
		try {
			(new SerializationIO<Traffic[]>()).writeClassObject(traffic, network+".data", true);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Start or stop all TimerTasks (TrafficTT and TimeTT).
	 * @param start Start or stop.
	 * @param delay How frequently TrafficTT should be scheduled not used when stopping the TimerTasks.
	 */
	private void startStop(boolean start, long delay){
		if(start){
			if(timer!=null){ // If not first run copy old to new, since the old can't be rescheduled.
				timeTT = new TimeTT(((TimeTT) timeTT).dates);
				trafficTT = new TrafficTT((TrafficTT) trafficTT);
			}
			timer = new Timer();
			timer.scheduleAtFixedRate(timeTT, 10, TIME_DELAY);
			timer.scheduleAtFixedRate(trafficTT, delay, delay);
		} else {
			timer.cancel();
		}
	}
	
	private class TimeTT extends TimerTask{
		private int[] dates = null;
		
		private TimeTT(int[] dates){
			this.dates = dates;
		}
		
		@Override
		public void run(){
			if(!Arrays.equals(dates, Time.getInstance().getDates())){
				dates = Time.getInstance().getDates();
				for(Traffic traffic : Worker.this.traffic)
					traffic.changeDate(dates);
			}
		}
	}
	
	/**
	 * Update traffic for all instances of Traffic, TrafficBuffer and TrafficStrings,
	 * the update is how much the traffic has increased since last run. It also
	 * notifies those observers that have register for a specific event, if there
	 * have occurred any changes in these classes. Traffic observers are notified
	 * every run.
	 */
	private class TrafficTT extends TimerTask{
		private long[] old = null;
		private String network = null;
		private LinkedList<TrafficObserver> tob = null;
		private LinkedList<TrafficBufferObserver> toob = null;
		private LinkedList<TrafficStringObserver> tso = null;
		
		/**
		 * Creates a new instance of TrafficTT that will read
		 * traffic from specified network and report this information
		 * to all Traffic, TrafficBuffer and TrafficStrings instances
		 * that exist in Worker's arrays for 'traffic', 'buffer' and 'strings'.
		 * @param network
		 */
		private TrafficTT(String network){
			this.network = network;
			try {
				old = Utility.readTraffic(network);
			} catch (FileNotFoundException e) {
				monitor.halt("CRITICAL ERROR - TERMINATING "+e.getMessage());
			} catch (InterfaceNotFoundException e) {
				monitor.halt(e.getMessage());
			}
			tob = new LinkedList<TrafficObserver>();
			toob = new LinkedList<TrafficBufferObserver>();
			tso = new LinkedList<TrafficStringObserver>();
		}
		
		/**
		 * Creates an instance of TrafficTT that is a
		 * complete copy one used as input. Used because
		 * a canceled TimerTask can't be rescheduled.
		 * @param t TrafficTT that should be copied.
		 */
		private TrafficTT(TrafficTT t){
			network = t.network;
			old = t.old;
			tob = t.tob;
			toob = t.toob;
			tso = t.tso;
		}
		
		/**
		 * Register a traffic buffer observer.
		 * @param ob Observer to be registered.
		 */
		private synchronized void addTrafficBufferObserver(TrafficBufferObserver ob){
			toob.add(ob);
		}
		
		/**
		 * Register a traffic observer.
		 * @param ob Observer to be registered.
		 */
		private synchronized void addTrafficObserver(TrafficObserver ob){
			tob.add(ob);
		}
		
		/**
		 * Register a traffic string observer.
		 * @param ob Observer to be registered.
		 */
		public synchronized void addTrafficStringObserver(TrafficStringObserver ob){
			tso.add(ob);
		}
		
		/**
		 * Notifies all registered Traffic observers that the traffic data
		 * have been updated.
		 * @param traffic Updated Traffic.
		 */
		private void notifyTrafficObserver(TrafficPackage[] traffic){
			for(TrafficObserver ob : tob)
				ob.trafficUpdated(traffic);
		}
		
		/**
		 * Notifies all registered Traffic buffer observers that the buffer values
		 * have been updated.
		 * @param buffer Updated Traffic buffer.
		 */
		private void notifyTrafficBufferObserver(TrafficBufferPackage[] buffer){
			for(TrafficBufferObserver ob : toob)
				ob.utilityUpdated(buffer);
		}
		
		/**
		 * Notifies all registered Traffic strings observers that one ore more strings
		 * have been added.
		 * @param trafficString Updated Traffic strings.
		 */
		private void notifyTrafficStringObserver(TrafficStringPackage trafficString){
			for(TrafficStringObserver ob : tso)
				ob.stringsUpdated(trafficString);
		}
		
		@Override
		public void run() {	
			try {
				long[] data = Utility.readTraffic(network), store = data.clone();
				// Calculate the difference since last run.
				for (int i = 0; i < old.length; i++)
					data[i] -= old[i];
				for (int i = 0; i < data.length; i++) {
					traffic[i].updateTraffic(data[i]);
					buffer[i].updateTraffic(data[i]);
					strings[i].updateTraffic(data[i]);
					if(strings[i].updated())
						notifyTrafficStringObserver(strings[i]);
				}
				notifyTrafficObserver(traffic);
				if (buffer[0].added()) // if value added notify observers.
					notifyTrafficBufferObserver(buffer.clone());
				old = store.clone();
			} catch (FileNotFoundException e) {
				monitor.halt("CRITICAL ERROR - TERMINATING: "+e.getMessage());
				this.cancel();
			} catch (InterfaceNotFoundException e) {
				monitor.halt(e.getMessage());
				this.cancel();
			}
		}
	}
}